<?php
/**
 * Created by PhpStorm
 * User: Kong
 * Date: 2025/9/23
 * Time: 14:28
 */

namespace App\Utils;

use function Hyperf\Support\env;

class MD5Token
{
    /**
     * Redis实例
     * @var \App\Utils\RedisClient
     */
    private mixed $redis;

    /**
     * @var string 密钥
     */
    private string $secretKey;

    /**
     * @var int 默认过期时间（秒）
     */
    private int $expireTime;

    /**
     * @var string Redis中token的前缀
     */
    private string $scene;

    /**
     * 构造函数
     * @param int $expireTime 过期时间（秒）
     * @param string $scene Redis键前缀
     */
    public function __construct(string $scene = 'admin_token:',int $expireTime = 3600)
    {
        $this->redis = \App\Utils\RedisClient::getInstance();
        $this->secretKey = env('SECRET_KEY',"978952042");
        $this->expireTime = $expireTime;
        $this->scene = $scene;
    }

    /**
     * 生成Token并存储到Redis
     * @param string $identifier 唯一标识符（如用户ID）
     * @param array $data 需要包含在token中的数据
     * @param null $expireTime 过期时间（秒），null表示使用默认值
     * @return string 生成的token
     * @throws \RedisException
     */
    public function generateToken(string $identifier,array $data = [], $expireTime = null)
    {
        // 设置默认过期时间
        $expireTime = $expireTime ?: $this->expireTime;

        // 添加时间戳和标识符
        $data['identifier'] = $identifier;
        $data['timestamp'] = time();

        // 将数据转换为字符串并排序以确保一致性
        ksort($data);
        $dataString = http_build_query($data);

        // 生成MD5签名
        $signature = md5($dataString . $this->secretKey);

        // 生成唯一token
        $token = md5(uniqid($identifier, true) . $signature . microtime(true));

        // 存储到Redis
        $tokenData = [
            'data' => $data,
            'signature' => $signature,
            'expire_time' => $expireTime
        ];

        $redisKey = $this->scene . $token;
        $this->redis->setex($redisKey, $expireTime, json_encode($tokenData));

        // 同时将token添加到标识符集合中，便于批量操作
        $identifierKey = $this->scene . 'identifier:' . $identifier;
        $this->redis->sadd($identifierKey, $token);
        $this->redis->expire($identifierKey, $expireTime);

        return $token;
    }

    /**
     * 验证Token
     * @param string $token 待验证的token
     * @return array|false 验证成功返回数据，失败返回false
     * @throws \RedisException
     */
    public function verifyToken(string $token)
    {
        $redisKey = $this->scene . $token;
        $tokenDataJson = $this->redis->get($redisKey);

        if (!$tokenDataJson) {
            throw new \Exception(\App\Constants\ErrorCode::getMessage(\App\Constants\ErrorCode::TOKEN_EXPIRED), \App\Constants\ErrorCode::TOKEN_EXPIRED);
        }

        $tokenData = json_decode($tokenDataJson, true);

        if (!isset($tokenData['data']) || !isset($tokenData['signature'])) {
            throw new \Exception(\App\Constants\ErrorCode::getMessage(\App\Constants\ErrorCode::TOKEN_INVALID), \App\Constants\ErrorCode::TOKEN_INVALID);
        }

        $data = $tokenData['data'];
        $providedSignature = $tokenData['signature'];

        // 检查是否过期
        if (isset($data['timestamp']) && (time() - $data['timestamp']) > $tokenData['expire_time']) {
            $this->revokeToken($token);
            throw new \Exception(\App\Constants\ErrorCode::getMessage(\App\Constants\ErrorCode::TOKEN_EXPIRED), \App\Constants\ErrorCode::TOKEN_EXPIRED);
        }

        // 重新生成签名进行比对
        ksort($data);
        $dataString = http_build_query($data);
        $expectedSignature = md5($dataString . $this->secretKey);

        // 验证签名
        if ($providedSignature != $expectedSignature) {
            throw new \Exception(\App\Constants\ErrorCode::getMessage(\App\Constants\ErrorCode::SIGN_VERIFY_FAILED), \App\Constants\ErrorCode::SIGN_VERIFY_FAILED);
        }
        return $data;
    }

    /**
     * 撤销Token（登出）
     * @param string $token 需要撤销的token
     * @return bool 操作是否成功
     * @throws \RedisException
     */
    public function revokeToken(string $token)
    {
        $redisKey = $this->scene . $token;
        $tokenDataJson = $this->redis->get($redisKey);

        if ($tokenDataJson) {
            $tokenData = json_decode($tokenDataJson, true);
            if (isset($tokenData['data']['identifier'])) {
                $identifier = $tokenData['data']['identifier'];
                $identifierKey = $this->scene . 'identifier:' . $identifier;
                $this->redis->srem($identifierKey, $token);
            }
        }

        return $this->redis->del($redisKey) > 0;
    }

    /**
     * 撤销指定标识符的所有Token（如用户登出所有设备）
     * @param string $identifier 标识符
     * @return int 被删除的token数量
     * @throws \RedisException
     */
    public function revokeAllTokens(string $identifier)
    {
        $identifierKey = $this->scene . 'identifier:' . $identifier;
        $tokens = $this->redis->smembers($identifierKey);

        $deletedCount = 0;
        foreach ($tokens as $token) {
            $redisKey = $this->scene . $token;
            $deletedCount += $this->redis->del($redisKey);
        }

        $this->redis->del($identifierKey);
        return $deletedCount;
    }

    /**
     * 延长Token有效期
     * @param string $token token
     * @param int $expireTime 新的过期时间（秒），null表示使用默认值
     * @return bool 操作是否成功
     * @throws \RedisException
     * @throws \Exception
     * @return bool
     */
    public function refreshToken(string $token,int $expireTime = 3600):bool
    {
        $expireTime = $expireTime ?: $this->expireTime;
        $redisKey = $this->scene . $token;
        $tokenDataJson = $this->redis->get($redisKey);
        if (!$tokenDataJson) {
            throw new \Exception(\App\Constants\ErrorCode::getMessage(\App\Constants\ErrorCode::TOKEN_EXPIRED), \App\Constants\ErrorCode::TOKEN_EXPIRED);
        }

        $tokenData = json_decode($tokenDataJson, true);
        $tokenData['expire_time'] = $expireTime;
        $data = $tokenData['data'];
        $data['extend_time'] = time();
        $tokenData['data'] = $data;
        // 重新生成签名，因为数据发生了变化
        ksort($data);
        $dataString = http_build_query($data);
        $tokenData['signature'] = md5($dataString . $this->secretKey);

        $result = $this->redis->setex($redisKey, $expireTime, json_encode($tokenData));

        // 同时更新标识符集合的过期时间
        if ($result && isset($data['identifier'])) {
            $identifierKey = $this->scene . 'identifier:' . $data['identifier'];
            $this->redis->expire($identifierKey, $expireTime);
        }

        return $result;
    }

    /**
     * 检查Token是否存在（不验证有效性）
     * @param string $token token
     * @return bool 是否存在
     * @throws \RedisException
     */
    public function hasToken(string $token): bool
    {
        $redisKey = $this->scene . $token;
        return $this->redis->exists($redisKey);
    }

    /**
     * 获取Token剩余过期时间
     * @param string $token token
     * @return int 剩余秒数，-1表示不存在或已过期
     * @throws \RedisException
     */
    public function getTTL(string $token)
    {
        $redisKey = $this->scene . $token;
        return $this->redis->ttl($redisKey);
    }

    /**
     * 获取指定标识符的活跃Token数量
     * @param string $identifier 标识符
     * @return int Token数量
     * @throws \RedisException
     */
    public function getTokenCount(string $identifier)
    {
        $identifierKey = $this->scene . 'identifier:' . $identifier;
        return $this->redis->scard($identifierKey);
    }
}